1
|
|
|
/* xlsx.js (C) 2013-present SheetJS -- http://sheetjs.com */ |
2
|
|
|
/* Notes: |
3
|
|
|
- usage: `ReactDOM.render( <SheetJSApp />, document.getElementById('app') );` |
4
|
|
|
- xlsx.full.min.js is loaded in the head of the HTML page |
5
|
|
|
- this script should be referenced with type="text/babel" |
6
|
|
|
- babel.js in-browser transpiler should be loaded before this script |
7
|
|
|
*/ |
8
|
|
|
class SheetJSApp extends React.Component { |
9
|
|
|
constructor(props) { |
10
|
|
|
super(props); |
11
|
|
|
this.state = { |
12
|
|
|
data: [], /* Array of Arrays e.g. [["a","b"],[1,2]] */ |
13
|
|
|
cols: [] /* Array of column objects e.g. { name: "C", K: 2 } */ |
14
|
|
|
}; |
15
|
|
|
this.handleFile = this.handleFile.bind(this); |
16
|
|
|
this.exportFile = this.exportFile.bind(this); |
17
|
|
|
}; |
18
|
|
|
handleFile(file/*:File*/) { |
19
|
|
|
/* Boilerplate to set up FileReader */ |
20
|
|
|
const reader = new FileReader(); |
21
|
|
|
const rABS = !!reader.readAsBinaryString; |
22
|
|
|
reader.onload = (e) => { |
23
|
|
|
/* Parse data */ |
24
|
|
|
const bstr = e.target.result; |
25
|
|
|
const wb = XLSX.read(bstr, {type:rABS ? 'binary' : 'array'}); |
26
|
|
|
/* Get first worksheet */ |
27
|
|
|
const wsname = wb.SheetNames[0]; |
28
|
|
|
const ws = wb.Sheets[wsname]; |
29
|
|
|
/* Convert array of arrays */ |
30
|
|
|
const data = XLSX.utils.sheet_to_json(ws, {header:1}); |
31
|
|
|
/* Update state */ |
32
|
|
|
this.setState({ data: data, cols: make_cols(ws['!ref']) }); |
33
|
|
|
}; |
34
|
|
|
if(rABS) reader.readAsBinaryString(file); else reader.readAsArrayBuffer(file); |
35
|
|
|
}; |
36
|
|
|
exportFile() { |
37
|
|
|
/* convert state to workbook */ |
38
|
|
|
const ws = XLSX.utils.aoa_to_sheet(this.state.data); |
39
|
|
|
const wb = XLSX.utils.book_new(); |
40
|
|
|
XLSX.utils.book_append_sheet(wb, ws, "SheetJS"); |
41
|
|
|
/* generate XLSX file and send to client */ |
42
|
|
|
XLSX.writeFile(wb, "sheetjs.xlsx") |
43
|
|
|
}; |
44
|
|
|
render() { return ( |
45
|
|
|
<DragDropFile handleFile={this.handleFile}> |
46
|
|
|
<div className="row"><div className="col-xs-12"> |
47
|
|
|
<DataInput handleFile={this.handleFile} /> |
48
|
|
|
</div></div> |
49
|
|
|
<div className="row"><div className="col-xs-12"> |
50
|
|
|
<button disabled={!this.state.data.length} className="btn btn-success" onClick={this.exportFile}>Export</button> |
51
|
|
|
</div></div> |
52
|
|
|
<div className="row"><div className="col-xs-12"> |
53
|
|
|
<OutTable data={this.state.data} cols={this.state.cols} /> |
54
|
|
|
</div></div> |
55
|
|
|
</DragDropFile> |
56
|
|
|
); }; |
57
|
|
|
}; |
58
|
|
|
|
59
|
|
|
if(typeof module !== 'undefined') module.exports = SheetJSApp |
60
|
|
|
|
61
|
|
|
/* -------------------------------------------------------------------------- */ |
62
|
|
|
|
63
|
|
|
/* |
64
|
|
|
Simple HTML5 file drag-and-drop wrapper |
65
|
|
|
usage: <DragDropFile handleFile={handleFile}>...</DragDropFile> |
66
|
|
|
handleFile(file:File):void; |
67
|
|
|
*/ |
68
|
|
|
class DragDropFile extends React.Component { |
69
|
|
|
constructor(props) { |
70
|
|
|
super(props); |
71
|
|
|
this.onDrop = this.onDrop.bind(this); |
72
|
|
|
}; |
73
|
|
|
suppress(evt) { evt.stopPropagation(); evt.preventDefault(); }; |
74
|
|
|
onDrop(evt) { evt.stopPropagation(); evt.preventDefault(); |
75
|
|
|
const files = evt.dataTransfer.files; |
76
|
|
|
if(files && files[0]) this.props.handleFile(files[0]); |
77
|
|
|
}; |
78
|
|
|
render() { return ( |
79
|
|
|
<div onDrop={this.onDrop} onDragEnter={this.suppress} onDragOver={this.suppress}> |
80
|
|
|
{this.props.children} |
81
|
|
|
</div> |
82
|
|
|
); }; |
83
|
|
|
}; |
84
|
|
|
|
85
|
|
|
/* |
86
|
|
|
Simple HTML5 file input wrapper |
87
|
|
|
usage: <DataInput handleFile={callback} /> |
88
|
|
|
handleFile(file:File):void; |
89
|
|
|
*/ |
90
|
|
|
class DataInput extends React.Component { |
91
|
|
|
constructor(props) { |
92
|
|
|
super(props); |
93
|
|
|
this.handleChange = this.handleChange.bind(this); |
94
|
|
|
}; |
95
|
|
|
handleChange(e) { |
96
|
|
|
const files = e.target.files; |
97
|
|
|
if(files && files[0]) this.props.handleFile(files[0]); |
98
|
|
|
}; |
99
|
|
|
render() { return ( |
100
|
|
|
<form className="form-inline"> |
101
|
|
|
<div className="form-group"> |
102
|
|
|
<label htmlFor="file">Spreadsheet</label> |
103
|
|
|
<input type="file" className="form-control" id="file" accept={SheetJSFT} onChange={this.handleChange} /> |
104
|
|
|
</div> |
105
|
|
|
</form> |
106
|
|
|
); }; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/* |
110
|
|
|
Simple HTML Table |
111
|
|
|
usage: <OutTable data={data} cols={cols} /> |
112
|
|
|
data:Array<Array<any> >; |
113
|
|
|
cols:Array<{name:string, key:number|string}>; |
114
|
|
|
*/ |
115
|
|
|
class OutTable extends React.Component { |
116
|
|
|
constructor(props) { super(props); }; |
117
|
|
|
render() { return ( |
118
|
|
|
<div className="table-responsive"> |
119
|
|
|
<table className="table table-striped"> |
120
|
|
|
<thead> |
121
|
|
|
<tr>{this.props.cols.map((c) => <th key={c.key}>{c.name}</th>)}</tr> |
122
|
|
|
</thead> |
123
|
|
|
<tbody> |
124
|
|
|
{this.props.data.map((r,i) => <tr key={i}> |
125
|
|
|
{this.props.cols.map(c => <td key={c.key}>{ r[c.key] }</td>)} |
126
|
|
|
</tr>)} |
127
|
|
|
</tbody> |
128
|
|
|
</table> |
129
|
|
|
</div> |
130
|
|
|
); }; |
131
|
|
|
}; |
132
|
|
|
|
133
|
|
|
/* list of supported file types */ |
134
|
|
|
const SheetJSFT = [ |
135
|
|
|
"xlsx", "xlsb", "xlsm", "xls", "xml", "csv", "txt", "ods", "fods", "uos", "sylk", "dif", "dbf", "prn", "qpw", "123", "wb*", "wq*", "html", "htm" |
136
|
|
|
].map(function(x) { return "." + x; }).join(","); |
137
|
|
|
|
138
|
|
|
/* generate an array of column objects */ |
139
|
|
|
const make_cols = refstr => { |
140
|
|
|
let o = [], C = XLSX.utils.decode_range(refstr).e.c + 1; |
141
|
|
|
for(var i = 0; i < C; ++i) o[i] = {name:XLSX.utils.encode_col(i), key:i} |
142
|
|
|
return o; |
143
|
|
|
}; |
144
|
|
|
|